package com.xiaomaoguai.fcp.pre.kepler.mock.dc;

import com.xiaomaoguai.fcp.pre.kepler.mock.properties.FeignMockProperties;
import com.xiaomaoguai.fcp.pre.kepler.mock.utils.FeignMethodInvocation;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.ParameterAnnotationsAttribute;
import javassist.bytecode.SignatureAttribute;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.MemberValue;
import javassist.bytecode.annotation.StringMemberValue;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.concurrent.atomic.AtomicLong;

/**
 * <a>https://www.cnblogs.com/coshaho/p/5105545.html</a>
 *
 * @author August.Zhang
 * @version v1.0.0
 * @date 2020/2/15 14:45
 * @since JDK 1.8
 */
public class FeignMockClientGenerator {

	private static final Logger log = LoggerFactory.getLogger(FeignMockClientGenerator.class);

	public static final AtomicLong CLASS_COUNTER = new AtomicLong(0);

	private static final String DC_CONVERT_CLIENT_NAME = DC.class.getName();

	private static final String DC_MOCK_HANDLER_NAME = DCMockHandler.class.getName();

	private static final String DC_MOCK_HANDLER_SIMPLE_NAME = DCMockHandler.class.getSimpleName();

	public static final String REQUEST_PARAM_TYPE_NAME = RequestParam.class.getName();

	public static final String REQUEST_BODY_TYPE_NAME = RequestBody.class.getName();

	/**
	 * 类池
	 */
	private static ClassPool pool;

	/*
	 * 初始化
	 */
	static {
		pool = ClassPool.getDefault();
		pool.insertClassPath(new ClassClassPath(FeignMockClientGenerator.class));
	}

	/**
	 * 创建类
	 * 后置处理-动态生成类-代理成bean
	 *
	 * @param feignClass 装类
	 * @return {@link javassist.CtClass}
	 */
	public static CtClass createClass(Class<?> feignClass) {
		String name = feignClass.getName();
		String convertClientName = name + DC_MOCK_HANDLER_SIMPLE_NAME;
		//动态构建类信息
		CtClass ctClass = null;
		try {
			ctClass = pool.get(convertClientName);
		} catch (NotFoundException e) {
			log.debug(convertClientName + " not found, now create this!");
			try {
				//创建接口
				ctClass = pool.makeInterface(convertClientName);
				ctClass.addInterface(pool.get(name));
				ctClass.addInterface(pool.get(DC_CONVERT_CLIENT_NAME));
				if (ctClass.isFrozen()) {
					ctClass.defrost();
				}
			} catch (NotFoundException ex) {
				log.error("create class " + convertClientName + " error", ex);
			}
		}


		return ctClass;
	}

	/**
	 * 后置处理-动态生成类-代理成bean
	 */
	public static CtClass createClassMethod(Class<?> feignClass, Method method, FeignMethodInvocation feignMethodInvocation, FeignMockProperties feignMockProperties) throws NotFoundException {
		//生成文件
		createGroovyFile(feignMockProperties, feignClass, method);

		CtMethod ctMethod = getMatcherCtMethod(feignClass, method);

		FeignMethodParamInfo[] paramInfos = getParamInfo(feignClass, method, ctMethod);

		String className = feignClass.getName();
		String name = feignClass.getSimpleName();

		String methodName = method.getName();
		String dcMockHandlerName = name + "$" + methodName + "$" + CLASS_COUNTER.getAndIncrement() + "$" + DC_MOCK_HANDLER_SIMPLE_NAME;

		feignMethodInvocation.setInvocationClass(feignClass);
		feignMethodInvocation.setClassName(className);
		feignMethodInvocation.setMethodName(methodName);
		feignMethodInvocation.setParameterTypes(method.getParameterTypes());
		feignMethodInvocation.setMethodParamInfos(paramInfos);

		//动态构建类信息
		CtClass ctClass = null;
		try {
			ctClass = pool.get(dcMockHandlerName);
		} catch (NotFoundException e) {
			try {
				log.debug(dcMockHandlerName + " not found, now create this!");
				//创建接口
				ctClass = pool.makeClass(dcMockHandlerName);
				ctClass.setSuperclass(pool.get(DC_MOCK_HANDLER_NAME));

				// 创建方法
				//1、返回参数类型
				CtClass ccReturnType = getReturnType(method, feignMockProperties);
				//2、参数类型
				Class<?>[] parameterTypes = method.getParameterTypes();
				int length = parameterTypes.length;
				CtClass[] ctClasses = new CtClass[length];
				for (int i = 0; i < length; i++) {
					ctClasses[i] = pool.get(parameterTypes[i].getName());
				}
				// 参数：  1：返回类型  2：方法名称  3：传入参数类型  4：所属类CtClass
				ctMethod = new CtMethod(ccReturnType, methodName, ctClasses, ctClass);
				ctMethod.setModifiers(Modifier.PUBLIC);
				StringBuilder body = new StringBuilder("{");
				body.append("\n Object[] param = new Object[").append(length).append("];");
				for (int i = 0; i < length; i++) {
					body.append("\n param[").append(i).append("] = $").append(i + 1).append(";");
				}
				body.append("\n  return mockExecutionHandler.execute($0.feignMethodInvocation,param);");
				body.append("\n}");
				ctMethod.setBody(body.toString());

				// 添加方法注解
				ConstPool constPool = ctClass.getClassFile().getConstPool();
				AnnotationsAttribute methodAttribute = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
				Annotation methodAnnotation = new Annotation(ResponseBody.class.getName(), constPool);
				methodAttribute.addAnnotation(methodAnnotation);

				ctMethod.getMethodInfo().addAttribute(methodAttribute);

				if (paramInfos != null) {
					ParameterAnnotationsAttribute parameterAttribute = new ParameterAnnotationsAttribute(constPool, ParameterAnnotationsAttribute.visibleTag);
					// waring : 这里要写 1 ,头大
					Annotation[][] paramArrays = new Annotation[length][1];
					for (int i = 0; i < paramInfos.length; i++) {
						FeignMethodParamInfo paramInfo = paramInfos[i];
						if (paramInfo != null) {
							paramArrays[i][0] = new Annotation(paramInfo.getType(), constPool);
							if (StringUtils.isNotBlank(paramInfo.getValue())) {
								paramArrays[i][0].addMemberValue("value", new StringMemberValue(paramInfo.getValue(), constPool));
							}
						}
					}
					parameterAttribute.setAnnotations(paramArrays);
					ctMethod.getMethodInfo().addAttribute(parameterAttribute);
				}

				ctClass.addMethod(ctMethod);
				if (ctClass.isFrozen()) {
					ctClass.defrost();
				}
			} catch (Exception ex) {
				log.error("create Dc class error", e);
			}
		}
		return ctClass;
	}

	/**
	 * 创建Groovy文件
	 *
	 * @param feignMockProperties 假装嘲笑属性
	 * @param feignClass          装类
	 * @param method              方法
	 */
	private static void createGroovyFile(final FeignMockProperties feignMockProperties, final Class<?> feignClass, final Method method) {
		String groovyFilePath = feignMockProperties.getGroovyFilePath();
		if (feignMockProperties.isDebug() && StringUtils.isNotBlank(groovyFilePath)) {
			Class<?> realClass = feignClass;
			Class<?>[] interfaces = feignClass.getInterfaces();
			if (interfaces.length > 0) {
				realClass = interfaces[0];
			}
			String groovyName = realClass.getSimpleName() + "_" + method.getName();
			GroovyMockTemplate.createTemplate(groovyFilePath, groovyName);
		}
	}

	/**
	 * 得到参数信息
	 *
	 * @param feignClass 装类
	 * @param method     方法
	 * @param ctMethod   ct方法
	 * @return {@link FeignMethodParamInfo[]}
	 */
	private static FeignMethodParamInfo[] getParamInfo(final Class<?> feignClass, final Method method, final CtMethod ctMethod) {
		FeignMethodParamInfo[] paramInfos = null;
		MethodInfo methodInfo = ctMethod.getMethodInfo();
		//调式5小时，要这么写
		ParameterAnnotationsAttribute parameterAnnotationsAttribute = (ParameterAnnotationsAttribute) methodInfo.getAttribute("RuntimeVisibleParameterAnnotations");
		//一维表示几个参数，二维表示几个注解
		if (parameterAnnotationsAttribute != null) {
			Annotation[][] annotations = parameterAnnotationsAttribute.getAnnotations();
			paramInfos = new FeignMethodParamInfo[annotations.length];
			for (int i = 0; i < annotations.length; i++) {
				for (int j = 0; j < annotations[i].length; j++) {
					if (StringUtils.equals(REQUEST_BODY_TYPE_NAME, annotations[i][j].getTypeName())) {
						paramInfos[i] = new FeignMethodParamInfo();
						paramInfos[i].setType(REQUEST_BODY_TYPE_NAME);
						break;
					} else if (StringUtils.equals(REQUEST_PARAM_TYPE_NAME, annotations[i][j].getTypeName())) {
						MemberValue memberValue1 = annotations[i][j].getMemberValue("value");
						MemberValue memberValue2 = annotations[i][j].getMemberValue("name");
						String key = memberValue1 instanceof StringMemberValue ? ((StringMemberValue) memberValue1).getValue() : ((StringMemberValue) memberValue2).getValue();
						paramInfos[i] = new FeignMethodParamInfo();
						paramInfos[i].setType(REQUEST_PARAM_TYPE_NAME);
						paramInfos[i].setValue(key);
						break;
					} else {
						log.warn("none @RequestParam or @RequestBody on this method,class: {},method: {}", feignClass, method);
					}
				}
			}
		}
		return paramInfos;
	}

	/**
	 * 找到方法，根据方法去取注解信息,耗时1天，妈蛋
	 *
	 * @param feignClass feign接口
	 * @param method     feign方法
	 * @return CtMethod
	 * @throws javassist.NotFoundException NotFoundException
	 */
	private static CtMethod getMatcherCtMethod(final Class<?> feignClass, final Method method) throws NotFoundException {
		CtMethod ctMethod = null;
		String methodName = method.getName();
		Class<?>[] parameterTypes = method.getParameterTypes();
		CtClass[] params = null;
		if (parameterTypes.length > 0) {
			params = new CtClass[parameterTypes.length];
			for (int i = 0; i < parameterTypes.length; i++) {
				params[i] = pool.get(parameterTypes[i].getName());
			}
		}
		Class<?>[] interfaces = feignClass.getInterfaces();
		if (interfaces != null && interfaces.length >= 1) {
			int findSuper = 0;
			do {
				try {
					CtClass cc = pool.get(feignClass.getInterfaces()[findSuper].getName());
					ctMethod = params != null ? cc.getDeclaredMethod(methodName, params) : cc.getDeclaredMethod(methodName);
				} catch (NotFoundException e) {
					findSuper++;
				}
			} while (ctMethod == null && findSuper <= interfaces.length - 1);
		} else {
			CtClass cc = pool.get(feignClass.getName());
			ctMethod = params != null ? cc.getDeclaredMethod(methodName, params) : cc.getDeclaredMethod(methodName);
		}
		if (ctMethod == null) {
			throw new NotFoundException("not found MatcherCtMethod,please check check");
		}
		return ctMethod;
	}


	/**
	 * 得到返回类型
	 *
	 * @param method              方法
	 * @param feignMockProperties 假装嘲笑属性
	 * @return {@link javassist.CtClass}* @throws Exception 异常
	 */
	private static CtClass getReturnType(Method method, FeignMockProperties feignMockProperties) throws Exception {
		Class<?> methodReturnType = method.getReturnType();
		if (methodReturnType.isAssignableFrom(void.class)) {
			return CtClass.voidType;
		}
		if (feignMockProperties.isObjectReturn()) {
			return pool.get(Object.class.getName());
		}
		String returnType = methodReturnType.getName();
		CtClass ccReturnType = pool.get(returnType);
		//添加泛型签名, TODO 这里有问题，真实泛型返回丢失 ,暂时都返回Object
		if (method.getGenericReturnType() instanceof ParameterizedType) {
			String genericClassName = ((ParameterizedType) method.getGenericReturnType()).getActualTypeArguments()[0].getTypeName();
			SignatureAttribute.TypeVariable typeVariable = new SignatureAttribute.TypeVariable(genericClassName);
			SignatureAttribute.MethodSignature ms = new SignatureAttribute.MethodSignature(null, null, typeVariable, null);
			ccReturnType.setGenericSignature(ms.encode());
		}
		return ccReturnType;
	}

}
